package scenery;

import com.crunch.core.Renderer;
import com.crunch.graphics.gpu.GpuBuffer;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuTexture;

import javax.media.opengl.GL;
import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;

/**
 * Instance of a tile layer which can be rendered.
 */
public class TileLayerInstance {
	public TileLayerInstance(TileLayer tileLayer, GpuTexture texture, Renderer renderer) {
		this.renderer = renderer;
		this.tileLayer = tileLayer;
		setTexture(texture);

		tileBuffers = new HashMap<Point, GpuBuffer>();

		renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				constructTileRegions(gpuState);
			}
		});
	}

	// queues resources to be released - should not make any more modifying calls after this
	public void release() {
		renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				if (texture != null) {
					texture.release();
				}

				for (GpuBuffer buffer : tileBuffers.values()) {
					buffer.release();
				}
			}
		});
	}

	public TileLayer getTileLayer() {
		return tileLayer;
	}

	public void setTexture(GpuTexture texture) {
		final GpuTexture texOld = this.texture;
		final GpuTexture texNew = texture;
		this.texture = texture;
		renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				if (texOld != null) {
					texOld.release();
				}
				if (texNew != null) {
					texNew.acquire();
				}
			}
		});
	}

	public GpuTexture getTexture() {
		return texture;
	}

	public void regionUpdated(int x, int y, int width, int height) {
		final int xF = x, yF = y, widthF = width, heightF = height;
		renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				updateTileRegions(gpuState, xF, yF, widthF, heightF);
			}
		});
	}

	public static final int POSITION_OFFSET         = 0;    // int[2]
	public static final int IMAGE_POSITION_OFFSET   = 8;    // short[2]
	public static final int SIZE_OFFSET             = 12;   // short[2]
	public static final int ANIMATION_OFFSET        = 16;   // short[3]
															// short[1] (padding)
	public static final int FRAME_OFFSET_OFFSET     = 24;   // short[2]
															// int[1] (padding)
	public static final int VERTEX_SIZE             = 32;

	private TileLayer tileLayer;
	private GpuTexture texture;
	private Renderer renderer;

	// width/height of a tile region
	private static int REGION_SIZE = 128;

	private void constructTileRegions(GpuState gpuState) {
		if (tileLayer.getTileCount() == 0) {
			return;
		}

		// find bounds covered by tiles
		int xMin = Integer.MAX_VALUE;
		int yMin = Integer.MAX_VALUE;
		int xMax = Integer.MIN_VALUE;
		int yMax = Integer.MIN_VALUE;
		for (int i = 0; i < tileLayer.getTileCount(); ++i) {
			TileLayer.Tile tile = tileLayer.getTile(i);
			xMin = Math.min(xMin, tile.getX());
			yMin = Math.min(yMin, tile.getY());
			xMax = Math.max(xMax, tile.getX() + tile.getWidth());
			yMax = Math.max(yMax, tile.getY() + tile.getHeight());
		}

		updateTileRegions(gpuState, xMin, yMin, xMax - xMin, yMax - yMin);
	}

	private void updateTileRegions(GpuState gpuState, int x, int y, int width, int height) {
		Point minOverallRegion = getRegionForPoint(x, y);
		Point maxOverallRegion = getRegionForPoint(x + width - 1, y + height - 1);

		// clear buffers in these regions
		Point p = new Point();
		for (p.y = minOverallRegion.y; p.y <= maxOverallRegion.y; ++p.y) {
			for (p.x = minOverallRegion.x; p.x <= maxOverallRegion.x; ++p.x) {
				GpuBuffer buffer = tileBuffers.remove(p);
				if (buffer != null) {
					buffer.release();
				}
			}
		}

		// first count up how many tiles should be in each region
		// use a size-1 array as a "mutable integer"
		HashMap<Point, int[]> regionTileCounts = new HashMap<Point, int[]>();
		for (int i = 0; i < tileLayer.getTileCount(); ++i) {
			TileLayer.Tile tile = tileLayer.getTile(i);
			Point minRegion = getRegionForPoint(tile.getX(), tile.getY());
			Point maxRegion = getRegionForPoint(tile.getX() + tile.getWidth() - 1, tile.getY() + tile.getHeight() - 1);
			minRegion.x = Math.max(minOverallRegion.x, minRegion.x);
			minRegion.y = Math.max(minOverallRegion.y, minRegion.y);
			maxRegion.x = Math.min(maxOverallRegion.x, maxRegion.x);
			maxRegion.y = Math.min(maxOverallRegion.y, maxRegion.y);

			for (p.y = minRegion.y; p.y <= maxRegion.y; ++p.y) {
				for (p.x = minRegion.x; p.x <= maxRegion.x; ++p.x) {
					int[] count = regionTileCounts.get(p);
					if (count == null) {
						// create new list with first element (count) set to 1
						regionTileCounts.put(new Point(p.x, p.y), new int[] { 1 });
					} else {
						// increment first element (count)
						++count[0];
					}
				}
			}
		}

		// now that we have the counts, create a ByteBuffer for each region
		HashMap<Point, ByteBuffer> regionTileData = new HashMap<Point, ByteBuffer>();
		for (Map.Entry<Point, int[]> e : regionTileCounts.entrySet()) {
			byte[] bufArray = new byte[e.getValue()[0] * VERTEX_SIZE];
			ByteBuffer buf = ByteBuffer.wrap(bufArray);
			buf.order(ByteOrder.nativeOrder()); // write in native byte order for OpenGL
			regionTileData.put(e.getKey(), buf);
		}

		// build data buffers
		for (int i = 0; i < tileLayer.getTileCount(); ++i) {
			TileLayer.Tile tile = tileLayer.getTile(i);
			Point minRegion = getRegionForPoint(tile.getX(), tile.getY());
			Point maxRegion = getRegionForPoint(tile.getX() + tile.getWidth() - 1, tile.getY() + tile.getHeight() - 1);
			minRegion.x = Math.max(minOverallRegion.x, minRegion.x);
			minRegion.y = Math.max(minOverallRegion.y, minRegion.y);
			maxRegion.x = Math.min(maxOverallRegion.x, maxRegion.x);
			maxRegion.y = Math.min(maxOverallRegion.y, maxRegion.y);

			for (p.y = minRegion.y; p.y <= maxRegion.y; ++p.y) {
				int regionMinY = p.y * REGION_SIZE;
				int regionMaxY = regionMinY + REGION_SIZE;
				for (p.x = minRegion.x; p.x <= maxRegion.x; ++p.x) {
					int regionMinX = p.x * REGION_SIZE;
					int regionMaxX = regionMinX + REGION_SIZE;

					ByteBuffer buffer = regionTileData.get(p);
					// clip the tile to the region
					int tileX = tile.getX();
					int tileY = tile.getY();
					short tileImageX = tile.getImageX();
					short tileImageY = tile.getImageY();
					short tileWidth = tile.getWidth();
					short tileHeight = tile.getHeight();
					if (tileX < regionMinX) {
						int diff = regionMinX - tileX;
						tileX += diff;
						tileImageX += diff;
						tileWidth -= diff;
					}
					if (tileX + tileWidth > regionMaxX) {
						int diff = tileX + tileWidth - regionMaxX;
						tileWidth -= diff;
					}
					if (tileY < regionMinY) {
						int diff = regionMinY - tileY;
						tileY += diff;
						tileImageY += diff;
						tileHeight -= diff;
					}
					if (tileY + tileHeight > regionMaxY) {
						int diff = tileY + tileHeight - regionMaxY;
						tileHeight -= diff;
					}

					// write tile data
					buffer.putInt(tileX); // 4
					buffer.putInt(tileY); // 8
					buffer.putShort(tileImageX); // 10
					buffer.putShort(tileImageY); // 12
					buffer.putShort(tileWidth); // 14
					buffer.putShort(tileHeight); // 16
					buffer.putShort(tile.getFrames()); // 18
					buffer.putShort(tile.getFrameTime()); // 20
					buffer.putShort(tile.getTimeOffset()); // 22
					buffer.putShort((short) 0); // 24 (padding)
					buffer.putShort(tile.getFrameOffsetX()); // 26
					buffer.putShort(tile.getFrameOffsetY()); // 28
					buffer.putInt(0); // 32 (padding)
				}
			}
		}

		// upload the data
		GpuBuffer.BufferDescription desc = new GpuBuffer.BufferDescription();
		desc.type = GL.GL_ARRAY_BUFFER;
		desc.usage = GL.GL_STATIC_DRAW;
		for (Map.Entry<Point, ByteBuffer> e : regionTileData.entrySet()) {
			desc.size = e.getValue().position();
			e.getValue().position(0);
			desc.data = e.getValue();
			GpuBuffer buffer = gpuState.createBuffer(desc);
			tileBuffers.put(e.getKey(), buffer);
		}
	}

	public Point getRegionForPoint(int x, int y) {
		Point point = new Point();
		// divide rounding down
		point.x = (x >= 0) ?
				x / REGION_SIZE :
				(x + 1 - REGION_SIZE) / REGION_SIZE;
		point.y = (y >= 0) ?
				y / REGION_SIZE :
				(y + 1 - REGION_SIZE) / REGION_SIZE;
		return point;
	}

	public GpuBuffer getRegionBuffer(Point region) {
		return tileBuffers.get(region);
	}

	// maps tile regions to GPU buffers containing tile data
	private HashMap<Point, GpuBuffer> tileBuffers;
}
